/******************************************************************************
 * Copyright 2020 The Firmament Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *****************************************************************************/

#include <firmament.h>

#include "drv_systick.h"
#include "hal/systick/systick.h"

#include "cpu_port.h"
#include "device.h"
#include "f28p65x_device.h"

static systick_dev_t systick_dev;

uint32_t SystemCoreClock = 0;

/**
 * This is the systick timer interrupt service routine.
 *
 */
__interrupt void rt_thread_tick_intr(void)
{
    /* enter interrupt */
    rt_interrupt_enter();

    hal_systick_isr(systick_dev);

    // rt_tick_increase();

    /* leave interrupt */
    rt_interrupt_leave();
}

static void SysTick_Config(rt_uint32_t freq)
{
    uint32_t timer_base = CPUTIMER2_BASE;

    uint32_t SystemClock = SysCtl_getClock(DEVICE_OSCSRC_FREQ);
    uint32_t prescaler   = CPUTIMER_CLOCK_PRESCALER_1;              // 设置分频因子
    uint32_t period      = SystemClock / ((prescaler + 1) * freq);  // 计算周期

    /* set timer0 interrupt */
    if (CPUTIMER0_BASE == timer_base)
    {
        SysCtl_enablePeripheral(SYSCTL_PERIPH_CLK_TIMER0);
        Interrupt_enable(INT_TIMER0);
    }
    else if (CPUTIMER1_BASE == timer_base)
    {
        SysCtl_enablePeripheral(SYSCTL_PERIPH_CLK_TIMER1);
        Interrupt_enable(INT_TIMER1);
    }
    else if (CPUTIMER2_BASE == timer_base)
    {
        SysCtl_enablePeripheral(SYSCTL_PERIPH_CLK_TIMER2);
        Interrupt_enable(INT_TIMER2);
    }
    else
    {
        RT_ASSERT(0);
        return;
    }

    CPUTimer_stopTimer(timer_base);                 // 停止定时器
    CPUTimer_setPeriod(timer_base, period - 1);     // 设置周期
    CPUTimer_setPreScaler(timer_base, prescaler);   // 设置定时器的时钟源
    CPUTimer_reloadTimerCounter(timer_base);        // 重载初始值
    CPUTimer_enableInterrupt(timer_base);           // 设置定时器的中断
    CPUTimer_startTimer(timer_base);                // 开启定时器
}

static void _set_systick_freq(rt_uint32_t freq)
{
    rt_uint32_t ClockFreq;
    rt_uint32_t TicksNum;

    RT_ASSERT(freq > 0);
    RT_ASSERT(systick_dev != NULL);

    SystemCoreClock = SysCtl_getClock(DEVICE_OSCSRC_FREQ);

    ClockFreq = SystemCoreClock;
    TicksNum = ClockFreq / freq;

    systick_dev->ticks_per_us = ClockFreq / 1e6;
    systick_dev->ticks_per_isr = TicksNum;

    SysTick_Config(freq);
}

static rt_err_t systick_configure(systick_dev_t systick, struct systick_configure* cfg)
{
    _set_systick_freq(cfg->tick_freq);

    systick->config = *cfg;

    return RT_EOK;
}

// TODO: return ticks number instead of us
static rt_uint32_t systick_read(systick_dev_t systick)
{
#if 1
    /* 获取定时器周期值（重载值） */
    volatile rt_uint32_t timer_period = CpuTimer2Regs.PRD.all;
    
    /* 获取当前计数器值 */
    volatile rt_uint32_t current_count = CpuTimer2Regs.TIM.all;
    
    /* 计算已过去的 ticks 数
       注意：C2000 定时器是递减计数器，所以计算方式与 SysTick 相同 */
    volatile rt_uint32_t elapsed_ticks = timer_period - current_count;

    return (elapsed_ticks / systick->ticks_per_us);
#else
    return (CPUTimer_getTimerCount(CPUTIMER2_BASE)) / systick->ticks_per_us;
#endif
}

const static struct systick_ops _systick_ops = {
    systick_configure,
    systick_read
};

rt_err_t drv_systick_init(void)
{
    static struct systick_device dev = {
        .ops = &_systick_ops,
        .config = SYSTICK_CONFIG_DEFAULT,
        .systick_isr_cb = RT_NULL
    };
    systick_dev = &dev;

    _set_systick_freq(dev.config.tick_freq);

    return hal_systick_register(systick_dev, "systick", RT_DEVICE_FLAG_RDONLY, RT_NULL);
}